package com.cloudlead.utils.persistence.codegen.serializer;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.validation.constraints.NotNull;

import com.cloudlead.common.lang.annotation.BelongsTo;
import com.cloudlead.common.lang.annotation.BelongsTos;
import com.cloudlead.common.lang.annotation.HasMany;
import com.cloudlead.common.lang.annotation.HasManys;
import com.cloudlead.common.lang.persistence.AbstractIdEntity;
import com.google.common.base.Function;
import com.mysema.codegen.CodeWriter;
import com.mysema.codegen.model.ClassType;
import com.mysema.codegen.model.Parameter;
import com.mysema.codegen.model.Type;
import com.mysema.codegen.model.TypeCategory;
import com.mysema.codegen.model.Types;
import com.mysema.query.codegen.EntityType;
import com.mysema.query.codegen.Property;
import com.mysema.query.codegen.Serializer;
import com.mysema.query.codegen.SerializerConfig;
import com.mysema.query.codegen.Supertype;
import com.mysema.util.BeanUtils;

public class SimpleVOSerializer implements Serializer {

	private static final Function<Property, Parameter> propertyToParameter = new Function<Property, Parameter>() {
		@Override
		public Parameter apply(Property input) {
			return new Parameter(input.getName(), input.getType());
		}
	};

	private final boolean propertyAnnotations;

	private final String javadocSuffix;

	private boolean addToString, addFullConstructor;

	private boolean printSupertype = false;

	private String validatorGroupClassName;

	/**
	 * Create a new BeanSerializer
	 */
	public SimpleVOSerializer() {
		this(true, " is a Codegen vo type");
	}

	/**
	 * Create a new BeanSerializer with the given javadoc suffix
	 *
	 * @param javadocSuffix
	 */
	public SimpleVOSerializer(String javadocSuffix) {
		this(true, javadocSuffix);
	}

	/**
	 * Create a new BeanSerializer
	 *
	 * @param propertyAnnotations
	 */
	public SimpleVOSerializer(boolean propertyAnnotations) {
		this(propertyAnnotations, " is a Codegen vo type");
	}

	/**
	 * Create a new BeanSerializer
	 *
	 * @param propertyAnnotations
	 * @param javadocSuffix
	 */
	public SimpleVOSerializer(boolean propertyAnnotations, String javadocSuffix) {
		this.propertyAnnotations = propertyAnnotations;
		this.javadocSuffix = javadocSuffix;
	}

	public void serialize(EntityType model, SerializerConfig serializerConfig,
			CodeWriter writer) throws IOException {
		Type argtype = new ClassType(String.class);
		Type type = new ClassType(AbstractIdEntity.class, argtype);
		Supertype entityType = new Supertype(type);
		model.addSupertype(entityType);
		String simpleName = model.getSimpleName();

		// package
		if (!model.getPackageName().isEmpty()) {
			writer.packageDecl(model.getPackageName());
		}

		// imports
		Set<String> importedClasses = getAnnotationTypes(model);
		for (Property property : model.getProperties()) {
			Type propertyType = property.getType();
			if (TypeCategory.LIST.equals(propertyType.getCategory())) {
				propertyType = propertyType.getParameters().get(0);
			} else if (TypeCategory.ARRAY.equals(propertyType.getCategory())) {
				propertyType = propertyType.getComponentType();
			}
			if (!propertyType.isPrimitive()) {
				importedClasses.add(propertyType.getFullName());
			}
		}
		if (model.hasLists()) {
			importedClasses.add(List.class.getName());
		}
		if (model.hasCollections()) {
			importedClasses.add(Collection.class.getName());
		}
		if (model.hasSets()) {
			importedClasses.add(Set.class.getName());
		}
		if (model.hasMaps()) {
			importedClasses.add(Map.class.getName());
		}
		if (addToString && model.hasArrays()) {
			importedClasses.add(Arrays.class.getName());
		}
		writer.importClasses(type.getFullName());
		writer.importClasses(validatorGroupClassName + ".*");
		writer.importClasses(importedClasses.toArray(new String[importedClasses
				.size()]));

		// javadoc
		writer.javadoc(simpleName + javadocSuffix);

		// header
		for (Annotation annotation : model.getAnnotations()) {
			if (annotation instanceof BelongsTos) {
				BelongsTos belongsTos = (BelongsTos) annotation;
				writer.beginLine("@"
						+ annotation.annotationType().getSimpleName()
						+ "(values = {");
				writer.beginLine("\n");
				for (int i = 0; i < belongsTos.values().length; i++) {
					BelongsTo belongsTo = belongsTos.values()[i];
					writer.append("\t");
					writer.annotation(belongsTo);
					if (i != belongsTos.values().length - 1) {
						writer.append(",");
					}
				}
				writer.beginLine("})");
				writer.beginLine("\n");
			} else if (annotation instanceof HasManys) {
				HasManys hasManys = (HasManys) annotation;
				writer.beginLine("@"
						+ annotation.annotationType().getSimpleName()
						+ "(values = {");
				writer.beginLine("\n");
				for (int i = 0; i < hasManys.values().length; i++) {
					HasMany hasMany = hasManys.values()[i];
					writer.append("\t");
					writer.annotation(hasMany);
					if (i != hasManys.values().length - 1) {
						writer.append(",");
					}
				}
				writer.beginLine("})");
				writer.beginLine("\n");
			} else {
				writer.annotation(annotation);
			}
		}

		if (printSupertype && model.getSuperType() != null) {
			writer.beginClass(model, model.getSuperType().getType());
		} else {
			writer.beginClass(model);
		}

		bodyStart(model, writer);

		if (addFullConstructor) {
			addFullConstructor(model, writer);
		}

		// fields
		for (Property property : model.getProperties()) {

			if (propertyAnnotations) {
				for (Annotation annotation : property.getAnnotations()) {
					if (NotNull.class.equals(annotation.annotationType())) {
						StringBuilder segments = new StringBuilder();
						if ("id".equalsIgnoreCase(property.getName())) {
							segments.append("@NotNull(groups={Update.class})");
						} else {
							segments.append("@NotNull(groups={Create.class,Update.class})");
						}
						writer.line(segments.toString());
					} else {
						writer.annotation(annotation);
					}
				}
			}
			writer.privateField(property.getType(), property.getEscapedName());
		}

		// accessors
		for (Property property : model.getProperties()) {
			String propertyName = property.getEscapedName();
			// getter
			writer.beginPublicMethod(property.getType(),
					"get" + BeanUtils.capitalize(propertyName));
			writer.line("return ", propertyName, ";");
			writer.end();
			// setter
			Parameter parameter = new Parameter(propertyName,
					property.getType());
			writer.beginPublicMethod(Types.VOID,
					"set" + BeanUtils.capitalize(propertyName), parameter);
			writer.line("this.", propertyName, " = ", propertyName, ";");
			writer.end();
		}

		if (addToString) {
			addToString(model, writer);
		}

		bodyEnd(model, writer);

		writer.end();
	}

	protected void addFullConstructor(EntityType model, CodeWriter writer)
			throws IOException {
		// public empty constructor
		writer.beginConstructor();
		writer.end();

		// full constructor
		writer.beginConstructor(model.getProperties(), propertyToParameter);
		for (Property property : model.getProperties()) {
			writer.line("this.", property.getEscapedName(), " = ",
					property.getEscapedName(), ";");
		}
		writer.end();
	}

	protected void addToString(EntityType model, CodeWriter writer)
			throws IOException {
		writer.beginPublicMethod(Types.STRING, "toString");
		StringBuilder builder = new StringBuilder();
		StringBuilder params = new StringBuilder();
		builder.append("String.format(\"" + model.getSimpleName());
		builder.append(" { ");
		int index = 0;
		for (Property property : model.getProperties()) {
			String propertyName = property.getEscapedName();
			builder.append(propertyName + " : %s");
			if (property.getType().getCategory() == TypeCategory.ARRAY) {
				params.append("Arrays.toString(" + propertyName + ")");
			} else {
				params.append(propertyName);
			}
			if (index != (model.getProperties().size() - 1)) {
				builder.append(",");
				params.append(",");
			}
			index++;
		}
		builder.append(" }\"");
		builder.append(",");
		builder.append(params);
		builder.append(")");
		writer.line(" return ", builder.toString(), ";");
		writer.end();
	}

	protected void bodyStart(EntityType model, CodeWriter writer)
			throws IOException {
		long currentTimeMillis = System.nanoTime();
		writer.beginLine("private static final long serialVersionUID =",
				currentTimeMillis + "L;");
		writer.beginLine("\n");

	}

	protected void bodyEnd(EntityType model, CodeWriter writer)
			throws IOException {
		// template method
	}

	private Set<String> getAnnotationTypes(EntityType model) {
		Set<String> imports = new HashSet<String>();
		for (Annotation annotation : model.getAnnotations()) {
			imports.add(annotation.annotationType().getName());
		}
		if (propertyAnnotations) {
			for (Property property : model.getProperties()) {
				for (Annotation annotation : property.getAnnotations()) {
					imports.add(annotation.annotationType().getName());
				}
			}
		}
		return imports;
	}

	public void setAddToString(boolean addToString) {
		this.addToString = addToString;
	}

	public void setAddFullConstructor(boolean addFullConstructor) {
		this.addFullConstructor = addFullConstructor;
	}

	public void setPrintSupertype(boolean printSupertype) {
		this.printSupertype = printSupertype;
	}

	public void setValidatorGroupClassName(String validatorGroupClassName) {
		this.validatorGroupClassName = validatorGroupClassName;
	}

}
